package edu.mcw.rgd.clinminer.web;

import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.CustomComponent;
import com.vaadin.ui.Layout;
import com.vaadin.ui.Table;
import com.vaadin.ui.VerticalSplitPanel;
import com.vaadin.ui.themes.Reindeer;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;

import org.apache.log4j.Logger;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.vaadin.dialogs.ConfirmDialog;
import org.vaadin.navigator.Navigator;

import com.vaadin.Application;
import com.vaadin.addon.jpacontainer.JPAContainer;
import com.vaadin.data.Buffered.SourceException;
import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.spring.roo.addon.annotations.RooVaadinAbstractEntityView;

import edu.mcw.rgd.clinminer.web.userrole.DummyEntityEditor;
import edu.mcw.rgd.clinminer.web.utils.NotificationInfoMiddle;

/**
 * Base class for that defines the common layout and some UI logic for all
 * entity views. This class is not specific to any entity class.
 */
@RooVaadinAbstractEntityView(useJpaContainer = true)
public abstract class AbstractEntityView<E> extends CustomComponent implements
		Navigator.View {
	private static final Logger log = Logger
			.getLogger(AbstractEntityView.class);

	private VerticalSplitPanel mainLayout;
	private Table table;
	private EntityEditor form;
	private Navigator navigator;
	private boolean dirty = false;

	/**
	 * Constructor for an abstract entity view.
	 *
	 * The methods {@link #createTable()} and {@link #createForm()} are used to
	 * create the main parts of the view.
	 */
	public AbstractEntityView() {
		// custom component size, must be set to allow inner layout take 100%
		// size
		setSizeFull();

		mainLayout = new VerticalSplitPanel();
		mainLayout.addStyleName("blue-bottom");
		mainLayout.setSplitPosition(30);
		setCompositionRoot(mainLayout);

		// table settings, display a fixed number of rows
		getTable().setSizeFull();
		getTable().setImmediate(true);
		getTable().setSelectable(true);
		getTable().addStyleName(Reindeer.TABLE_BORDERLESS);
		getTable().addStyleName(Reindeer.TABLE_STRONG);

		if (!(getForm() instanceof DummyEntityEditor)) {
			mainLayout.setFirstComponent(getTable());
			mainLayout.setSecondComponent(getForm());
		} else {
			mainLayout.setFirstComponent(getViewLayout());
			mainLayout.setSplitPosition(100);
			mainLayout.setLocked(true);
		}

		// hide buttons if certain operations are not allowed
		getForm().setSaveAllowed(isCreateAllowed() || isUpdateAllowed());
		getForm().setDeleteAllowed(isDeleteAllowed());

		// add listeners for the buttons
		addListeners();

		// initially nothing on the form
		setCurrentEntity(null);
	}

	// View interface and related

	@Override
	public void init(Navigator navigator, Application application) {
		this.navigator = navigator;
		// refresh table
		refresh();
	}

	@Override
	public void navigateTo(String requestedDataId) {
		// refresh table
		// TODO: why a refresh here?
		refresh();

		setDirty(false);

		if (null == requestedDataId || "".equals(requestedDataId)) {
			getTable().setValue(null);
			// continue
		} else if (isCreateAllowed() && "new".equals(requestedDataId)) {
			createNewEntity();
			return;
		} else if (isUpdateAllowed() && requestedDataId.startsWith("edit/")) {
			try {
				Long id = Long.valueOf(requestedDataId.substring(5));
				setCurrentEntity(getEntityForItem(getTable().getItem(id)));
				getTable().setValue(id);
				return;
			} catch (NumberFormatException e) {
				navigateToFragment(null);
				return;
			}
		}

		setCurrentEntity(null);
	}

	@Override
	public String getWarningForNavigatingFrom() {
		return (isDirty() && getForm().isModified()) ? "Discard unsaved changes?"
				: null;
	}

	protected void navigateToFragment(String fragment) {
		if (navigator != null) {
			String uri = navigator.getUri(getClass());
			// remove fragment
			uri = uri.replaceAll("/.*", "");
			if (fragment == null) {
				fragment = "";
			} else {
				fragment = "/" + fragment;
			}
			navigator.navigateTo(uri + fragment);
		}
	}

	protected void setDirty(boolean dirty) {
		this.dirty = dirty;
	}

	protected boolean isDirty() {
		return dirty;
	}

	// other methods

	public void createNewEntity() {
		if (isCreateAllowed()) {
			getTable().setValue(null);

			getForm().setVisible(true);

			getForm().setDeleteAllowed(false);
			getForm().setSaveAllowed(isCreateAllowed());
			setCurrentEntity(createEntityInstance());
			getForm().setCaption("New " + getEntityName());
			getForm().setDefaults();
		}
	}

	/**
	 * Adds listeners for the various buttons on the form and for table
	 * selection change.
	 */
	protected void addListeners() {
		getTable().addListener(new ValueChangeListener() {
			@Override
			public void valueChange(ValueChangeEvent event) {
				Object value = event.getProperty().getValue();
				if (value != null) {
					navigateToFragment("edit/"
							+ String.valueOf(value).replaceAll("[^0-9]", ""));
				}
			}
		});

		getForm().addSaveActionListener(new ClickListener() {
			@Override
			public void buttonClick(ClickEvent event) {
				if (doCommit()) {
					refresh();
					getForm().saveDefaults();
					setDirty(false);
					navigateToFragment(null);
					getApplication().getMainWindow().showNotification(
							new NotificationInfoMiddle("Saved", null));
				}
			}
		});
		getForm().addDeleteActionListener(new ClickListener() {
			@Override
			public void buttonClick(ClickEvent event) {
				ConfirmDialog.show(getApplication().getMainWindow(),
						"Please Confirm:",
						"Are you really sure you want to delete this?", "I am",
						"Not quite", new ConfirmDialog.Listener() {

							@Override
							public void onClose(ConfirmDialog dialog) {
								if (dialog.isConfirmed()) {
									doDelete();
									setDirty(false);
									navigateToFragment(null);
									getApplication().getMainWindow()
											.showNotification(
													new NotificationInfoMiddle(
															"Deleted", null));
								}
							}
						});
			}
		});
	}

	/**
	 * Set the current entity to display on the form and to edit.
	 *
	 * If the table contains an item with the entity as its key, that item is
	 * reused. Otherwise, a new Item is created.
	 *
	 * @param entity
	 */
	protected void setCurrentEntity(E entity) {
		getForm().setVisible(entity != null);
		if (entity != null) {
			getForm().refresh();
			getForm().setCaption("Edit " + getEntityName());

			boolean newEntity = isNewEntity(entity);
			getForm().setDeleteAllowed(isDeleteAllowed());
			boolean saveAllowed = newEntity ? isCreateAllowed()
					: isUpdateAllowed();
			getForm().setSaveAllowed(saveAllowed);
			setDirty(entity != null && saveAllowed);

			Item item = getItemForEntity(entity);

			getForm().setItemDataSource(item);
		} else {
			getForm().setItemDataSource(null);
		}
	}

	// getters for the components
	protected Table getTable() {
		if (table == null) {
			table = createTable();
		}
		return table;
	}

	protected EntityEditor getForm() {
		if (form == null) {
			form = createForm();
		}
		return form;
	}

	protected Layout getViewLayout() {
		throw new UnsupportedOperationException();
	}

	/**
	 * Creates the table listing the instances of this entity.
	 *
	 * Subclasses can override this.
	 *
	 * @return
	 */
	protected Table createTable() {
		Table table = new Table("Formatted Table") {
			private static final long serialVersionUID = 1L;

			@Override
			protected String formatPropertyValue(Object rowId, Object colId,
					Property property) {
				// Format by property type
				if (property.getType() == Date.class
						&& null != property.getValue()) {
					SimpleDateFormat df = new SimpleDateFormat("MM/dd/yyyy");
					return df.format((Date) property.getValue());
				}
				return super.formatPropertyValue(rowId, colId, property);
			}
		};

		table.setColumnCollapsingAllowed(true);
		table.setColumnReorderingAllowed(true);
		return table;
	}

	/**
	 * Refresh the table and any other relevant parts of the entity view.
	 *
	 * This is called after entities are saved or deleted or when navigating to
	 * a new view.
	 */
	protected void refresh() {
		Object sortContainerPropertyId = getTable()
				.getSortContainerPropertyId();
		boolean sortAscending = getTable().isSortAscending();

		configureTable(getTable());

		if (sortContainerPropertyId != null) {
			getTable().setSortAscending(sortAscending);
			getTable().setSortContainerPropertyId(sortContainerPropertyId);
			getTable().sort();
		}
	}

	/**
	 * Return the columns (property ids) to show in the entity table.
	 *
	 * @return array of property ids for the entity type
	 */
	public Object[] getTableColumns() {
		ArrayList<Object> columnIds = new ArrayList<Object>();
		for (Object property : getTable().getContainerPropertyIds()) {
			// stop adding db ids
			// if (property != null && property.equals(getIdProperty())) {
			// columnIds.add(0, property);
			if (property != null && !property.equals(getVersionProperty())
					&& !property.equals(getIdProperty())) {
				columnIds.add(property);
			}
		}
		return columnIds.toArray();
	}

	// these are typically defined in the ITD (aspect) of the subclass

	protected abstract Class<? extends E> getEntityClass();

	protected abstract String getEntityName();

	public abstract boolean isCreateAllowed();

	protected abstract boolean isUpdateAllowed();

	protected abstract boolean isDeleteAllowed();

	protected abstract boolean isNewEntity(E entity);

	protected abstract void deleteEntity(E entity);

	protected abstract E saveEntity(E entity);

	public abstract Object getIdProperty();

	public abstract Object getVersionProperty();

	protected abstract Container getTableContainer();

	/**
	 * Returns an existing or new Item for an entity to be edited on a form. For
	 * already stored entities, the item is retrieved from the table. For new
	 * entities, a new item is created.
	 *
	 * @param entity
	 * @return Item
	 */
	protected abstract Item getItemForEntity(E entity);

	/**
	 * Obtains the entity from an item in a container specific manner.
	 *
	 * @param item
	 *            the item for which to get the entity
	 * @return entity
	 */
	protected abstract E getEntityForItem(Item item);

	/**
	 * Returns the item identifier for an entity instance.
	 *
	 * @param entity
	 *            the entity for which to get the identifier
	 * @return Object item identifier
	 */
	public abstract Object getIdForEntity(E entity);

	/**
	 * Create a new entity instance.
	 *
	 * Concrete entity view classes must implement this method, and any special
	 * initialization of the entity can be performed here.
	 */
	protected abstract E createEntityInstance();

	// these are typically defined in the concrete subclass

	/**
	 * Create the form used for editing an entity.
	 *
	 * Concrete subclasses must implement this method.
	 *
	 * @return
	 */
	protected abstract EntityEditor createForm();

	protected abstract void configureTable(Table table);

	@Transactional
	public boolean doCommit() {
		try {
			getForm().commit();
			E entity = getEntityForItem(getForm().getItemDataSource());
			Object id = getIdForEntity(entity);
			if (id == null || !getTableContainer().containsId(id)) {
				((JPAContainer<E>) getTableContainer()).addEntity(entity);
			}
			return true;
		} catch (InvalidValueException e) {
			// show validation error also on the save button
			getForm().setCommitErrorMessage(e.getMessage());
			return false;
		} catch (ConstraintViolationException e) {
			for (ConstraintViolation<?> v : e.getConstraintViolations()) {
				getForm().setCommitErrorMessage(v.getMessage());
			}
			TransactionAspectSupport.currentTransactionStatus()
					.setRollbackOnly();
			return false;
		} catch (SourceException e) {
			// dewrap the exception
			Throwable t = e.getCause().getCause().getCause();
			if (t instanceof ConstraintViolationException) {
				for (ConstraintViolation<?> v : ((ConstraintViolationException) t)
						.getConstraintViolations()) {
					getForm().setCommitErrorMessage(v.getMessage());
				}
				TransactionAspectSupport.currentTransactionStatus()
						.setRollbackOnly();

			} else {
				// unexpected
				e.printStackTrace();
				throw new RuntimeException(e);
			}

			return false;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}
}
